﻿//#define bypassScientistConstraint
#define KIS
#if KIS
using KIS;
#endif
using Plexi.Configuration;
using Plexi.Science;
using Plexi.Tools;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace Plexi
{
	[KSPAddon(KSPAddon.Startup.Flight, false)]
	public class SurfaceRockSpawner : MonoBehaviour
	{
		#region static stuff
		public static RockData SelectedRockData;
		public static bool ExaminedByScientist { get; set; }
		#region private static stuff because they're also used in the AddRri method
		private static List<RealRockInfo> _realRockInfos;
		private static List<RockInfo> _rockInfos;
		private static List<GameObject> _fakeRocks;
		private static int _cx, _cy;
		#endregion
		#endregion

		#region constants
		private const string _rockPartNameBase = "rock", _resetConfirmText = "This will reset all rocks to their original place.", _disableConfirmText = "This will remove all rocks that haven't yet been moved.",
			_nonScientistRockExamination = "It looks like a block of something... heavy. It has a hard surface. Maybe a scientist would know more...",
			_nonScientistIceExamination = "It's a piece of ice, I think? Maybe a scientist would know more...",
			_strConfirmation = "Confirmation", _strExamination = "Examination", _mainWindowTitle = "PLEXI rocks", _strScientist = "scientist";
		private const int _fastCheckVelocity = 45, _slowCheckVelocity = 35, _maxRockMoveSpeed = 40, _rocksPerCell = 10;
		private const float _rockMoveThreshold = 0.00005f, _identicalRockPosThreshold = 0.0001f, _movedRockRemoveDistance = 300f, _movedRockRespawnDistance = 280f,
			_extraSpawnHeightMovable = 3f, _extraSpawnHeightImmovable = 0f, _reEnableAltitude = 180f, _disableAltitude = 250f, _fineCheckAltitude = 300f, _coarseCheckAltitude = 350f,
			 _reEnableAtDisableSpeedPercent = 0.90f, _initialReEnableSpeed = 80f, _slowCheckFrequencySeconds = 3.0f, _fastCheckFrequencySeconds = 1f, _rockMovingCheckFrequencySeconds = 1f,
			 _rockMoveMaxPlayerDistance = 0.001f, _unmovableRockProbability = 0.25f, _movableRockScaleMin = 0.2f, _movableRockScaleMax = 0.35f, _unmovableRockScaleMin = 0.4f, _unmovableRockScaleMax = 1.5f,
			 _undergroundRockThreshold = -20f, _lowerRockDensityChance = 0.2f, _maxDeactivatePercent = 0.5f, _fineCheckFrequencyMilliseconds = 200, _maxAnalyzeDistance = 3.5f,
			 _cellHasProminentRockChance = 0.3f, _maxExamineVelocity = 0.1f, _vesselChangeRestartDistance = 10f;
		/// <summary>
		/// The volume (in cubic meters) of a rock if its scale is 1. (Rock models should be roughly the same size!)
		/// Mass will be calculated accordingly, using the SpecificWeight, see RealRockInfo.CalculateMass().
		/// </summary>
		public const float BaseRockVolume = 0.5f;
		#endregion

		#region member variables
		private System.Random _r;
		private bool _enabled = false, _manualDisable = false, _fineChecks = false, _movingRocks = false, _disabling = false, _starting = false, _checkingMoves = false, _isMovedRock = false,
			_showMainWindow = false, _showConfirmWindow = false, _showExamineFailureWindow = false, _cellHasProminentRock = false;
		private bool? _cellHasMovedRock;
		private float _cellWidth, _cellWidthHalf, _maxSpeedInCell, _disableSpeed = 100f, _reEnableSpeed, _currentSpeed, _checkFrequencySeconds, _dtAccumulator, _dtAccumulator2, _dtAccumulator3, _dt, _ccLon, _ccLat;
		private int _cellX, _cellY, _pcx, _pcy, _xd, _yd, _counter, _baseSeed = 9431237/*Should be less than 2 billion b/c we need to add to it*/,
			_indexInCell, _activeRocksInCell, _maxDeactivatedRocksPerCell, _classSelector, _typeSelector;
		private AvailablePart _ap;
		private List<AvailablePart> _rockAvailableParts;
		private Rect _mainWindowRect = new Rect(600, 400, 155f, 1f), _confirmWindowRect = new Rect(800, 400, 300f, 1f), _partInfoWindowRect = new Rect(1000, 400, 200f, 1f),
			_analyzeFailureWindowRect = new Rect(800, 400, 300f, 1f);
		private Vessel _activeVessel, _prevActiveVessel, _rockVessel;
		private CelestialBody _body;
		private List<GameObject> _rockPrefabs;
		private Vector3d _ppos, _spawnSurfacePos;
		private Transform _spawnTransform;
		private double _elevation, _vesselElev, _rlon, _rlon2, _rlat, _vLat, _vLon, _vesselRealAltitude, _cellRockDensity, _extraSpawnHeight;
		private ShipConstruct _rockSc;
		private Part _rockPart;
		private RockInfo _ri;
		private RealRockInfo _rri;
		private GameObject _rockPrefab, _fr, _emptyGameObject = new GameObject();
		private string _statusText, _confirmationText;
		private ApplicationLauncherButton _guiButton;
		private RockPart _rp;
		private Texture _texture;
		private Renderer[] _renderers;
		private RockTypePercent _prominentInCell;
		private CBAttributeMapSO.MapAttribute _biome;
		private RockRandomization[] _rockRandoms;
		private RockRandomization _rockRand;
		private RaycastHit _hit;
		private string _examineFailureReason, _examineFailureTitle;

		//debug:
		private string _debugText = "";
		private int _debugMovedCellCount = 0, _debugActiveCount, _debugRealRockSpawnedCount = 0;
		private static int _debugRriCount;
		#endregion

		void Start()
		{
			Log.Add("start");
#if KIS
			Log.Add("KIS-enabled version.");
#else
			Log.Add("KIS-disabled version.");
#endif
			_maxDeactivatedRocksPerCell = (int)(_rocksPerCell * _maxDeactivatePercent);
			_reEnableSpeed = _initialReEnableSpeed;
			_rockRandoms = new RockRandomization[_rocksPerCell];
			for (int i = 0; i < _rocksPerCell; i++)
				_rockRandoms[i] = new RockRandomization();
			RockType.LoadFromResourceIfNecessary();
			ConfigLoader.LoadConfigIfNecessary();
			if (_guiButton != null)
			{
				ApplicationLauncher.Instance.RemoveModApplication(_guiButton);
				_guiButton = null;
			}
			_guiButton = ApplicationLauncher.Instance.AddModApplication(
					new RUIToggleButton.OnTrue(onButtonToggleOn),
					new RUIToggleButton.OnFalse(onButtonToggleOff),
					new RUIToggleButton.OnHover(() => { }),
					new RUIToggleButton.OnHoverOut(() => { }),
					new RUIToggleButton.OnEnable(() => { }),
					new RUIToggleButton.OnDisable(() => { }),
					ApplicationLauncher.AppScenes.FLIGHT,
					TextureManager.LoadToolbarIcon());
			_rockAvailableParts = new List<AvailablePart>();
			_rockPrefabs = new List<GameObject>();
			for (int i = 1; i <= _rocksPerCell; i++)
			{
				_ap = PartLoader.getPartInfoByName(string.Format("{0}{1}", _rockPartNameBase, i));
				if (_ap == null)
				{
					Log.Add("ERROR: Can't getPartInfoByName: {0}{1}", _rockPartNameBase, i);
					break;
				}
				if (_ap.partPrefab == null)
				{
					Log.Add("ERROR: _ap.partPrefab == null: {0}{1}", _rockPartNameBase, i);
					break;
				}
				_rockAvailableParts.Add(_ap);
				MeshFilter mf = _ap.partPrefab.FindModelComponent<MeshFilter>();
				Renderer r = _ap.partPrefab.FindModelComponent<Renderer>();
				_rockPrefab = new GameObject();
				_rockPrefab.AddComponent<MeshRenderer>();
				_rockPrefab.AddComponent<MeshFilter>();
				_rockPrefab.GetComponent<MeshFilter>().sharedMesh = mf.sharedMesh;
				var rbody = _rockPrefab.AddComponent<Rigidbody>();
				rbody.mass = 0.01f;
				rbody.angularDrag = 5f;
				_rockPrefab.AddComponent<MeshCollider>();
				_rockPrefab.GetComponent<MeshCollider>().convex = true;
				_rockPrefab.collider.material = new PhysicMaterial
				{
					frictionCombine = PhysicMaterialCombine.Maximum,
					bounceCombine = PhysicMaterialCombine.Minimum,
					bounciness = 0.45f,
					dynamicFriction = 0.05f,
					staticFriction = 0.25f
				};
				_rockPrefab.renderer.material = r.material;
				_rockPrefab.SetActiveReally(false);
				_rockPrefabs.Add(_rockPrefab);
			}
			_fakeRocks = new List<GameObject>();
			_rockInfos = new List<RockInfo>();
			_realRockInfos = new List<RealRockInfo>();
			StartCoroutine(TryEnable());
		}

		void OnGUI()
		{
			if (_showMainWindow)
			{
				if (_showConfirmWindow)
					_confirmWindowRect = KSPUtil.ClampRectToScreen(GUILayout.Window(124, _confirmWindowRect, DrawConfirmWindow, _strConfirmation));
				else if (_showExamineFailureWindow)
					_analyzeFailureWindowRect = KSPUtil.ClampRectToScreen(GUILayout.Window(126, _analyzeFailureWindowRect, DrawExamineFailureWindow, _examineFailureTitle));
				_mainWindowRect = KSPUtil.ClampRectToScreen(GUILayout.Window(123, _mainWindowRect, DrawMainWindow, _mainWindowTitle));
			}
			if (SelectedRockData != null && _activeVessel != null && _activeVessel.isEVA)
			{
#if bypassScientistConstraint
				_partInfoWindowRect = KSPUtil.ClampRectToScreen(GUILayout.Window(125, _partInfoWindowRect, SelectedRockData.DrawWindow, _strExamination));
#else
				if (ExaminedByScientist)
					_partInfoWindowRect = KSPUtil.ClampRectToScreen(GUILayout.Window(125, _partInfoWindowRect, SelectedRockData.DrawWindow, _strExamination));
				else
					_analyzeFailureWindowRect = KSPUtil.ClampRectToScreen(GUILayout.Window(126, _analyzeFailureWindowRect, DrawExamineFailureWindow, _strExamination));
#endif
			}
		}

		private void DrawMainWindow(int winid)
		{
			if (!_showMainWindow)
				return;
			GUILayout.BeginVertical();
			if (_activeVessel != null)
			{
				GUILayout.Label(string.Format("lat: {0}", _vLat));
				GUILayout.Label(string.Format("lon: {0}", _vLon));
				if (_activeVessel.isEVA)
					GUILayout.Label(string.Format("{0} is a(n) {1}.", _activeVessel.vesselName, _activeVessel.GetVesselCrew().First().experienceTrait.Config.Name));
				if (_movingRocks)
					_statusText = "moving";
				else if (_checkingMoves)
					_statusText = "checking moved rocks";
				else if (_starting)
					_statusText = "starting";
				else if (_disabling)
					_statusText = "clearing";
				else if (_enabled)
					_statusText = "enabled";
				else if (_manualDisable)
					_statusText = "disabled by user";
				else
					_statusText = string.Format("disabled, re-enabling at: {0} m, {1} m/s.vra:{2}", (int)_reEnableAltitude, (int)_reEnableSpeed, (int)_vesselRealAltitude);
				GUILayout.Label(string.Format("Status: {0}", _statusText));
				if (_enabled && _activeVessel.isEVA)
				{
					if (GUILayout.Button("Examine nearest rock"))
						ExamineNearestRock();
				}
				if (!_showConfirmWindow && !_movingRocks && !_disabling && !_starting)
				{
					if (_enabled)
					{
						if (GUILayout.Button("Disable"))
						{
							_confirmationText = _disableConfirmText;
							_showConfirmWindow = true;
							Log.Add("disable requested");
						}
					}
					else if (_manualDisable)
					{
						if (GUILayout.Button("Enable"))
						{
							_manualDisable = false;
							Log.Add("re-enable requested");
							StartCoroutine(TryEnable());
						}
					}
				}
			}
			if (GUILayout.Button("Close"))
				_showMainWindow = _showConfirmWindow = false;
			GUILayout.EndVertical();
			GUI.DragWindow();
		}

		private void DrawConfirmWindow(int winid)
		{
			if (!_showConfirmWindow)
				return;
			GUILayout.BeginVertical();
			GUILayout.Label("Are you sure?");
			GUILayout.Label(_confirmationText);
			if (GUILayout.Button("Do it"))
			{
				_showConfirmWindow = false;
				ManualDisable();
			}
			else if (GUILayout.Button("Cancel"))
				_showConfirmWindow = false;
			GUILayout.EndVertical();
			GUI.DragWindow();
		}

		private void DrawExamineFailureWindow(int id)
		{
			GUILayout.BeginVertical();
			GUILayout.Label(ExaminedByScientist ? _examineFailureReason : (SelectedRockData.Type.Class == RockClassifications.Ice ? _nonScientistIceExamination : _nonScientistRockExamination));
			if (GUILayout.Button("OK"))
			{
				_showExamineFailureWindow = false;
				SelectedRockData = null;
			}
			GUILayout.EndVertical();
			GUI.DragWindow();
		}

		private void onButtonToggleOn()
		{
			_showConfirmWindow = false;
			_showMainWindow = !_showMainWindow;
		}

		private void onButtonToggleOff()
		{
			_showConfirmWindow = false;
			_showMainWindow = !_showMainWindow;
		}

		private void DebugCountActive()
		{
			_debugActiveCount = 0;
			if (_fakeRocks != null)
			{
				foreach (var v in _fakeRocks)
				{
					if (v != null && v.activeInHierarchy)
						_debugActiveCount++;
				}
			}
		}

		/// <summary>Called often wether enabled or not, checks if we should disable or re-enable, or if we should check less often (if disabled).</summary>
		private bool FineCheck()
		{
			_activeVessel = FlightGlobals.ActiveVessel;
			if (_activeVessel == null)
			{
				if (_enabled)
				{
					Log.Add("Disabling because active vessel is null.");
					Disable();
				}
				return true;
			}
			SetPosLatLonElevAlt();
			if (!_manualDisable && _prevActiveVessel != null && !_activeVessel.Equals(_prevActiveVessel))
			{
				Log.Add("restarting because changed vessel from {0} to {1}.", _prevActiveVessel.vesselName, _activeVessel.vesselName);
				VesselChanged();
				return true;
			}
			_prevActiveVessel = _activeVessel;
			if (!_enabled && !_manualDisable)
			{
				if (_currentSpeed <= _reEnableSpeed && _vesselRealAltitude <= _reEnableAltitude)
					StartCoroutine(ReEnable());
				else if (_vesselRealAltitude > _coarseCheckAltitude)
				{
					_fineChecks = false;
					_dtAccumulator2 = 0.000f;
#if DEBUG
					Log.Add("(disabled): Vessel altitude is {0}, switching to CoarseCheck.", _vesselRealAltitude);
#endif
				}
				return true;
			}
			else if (_vesselRealAltitude > _disableAltitude)
			{
				Log.Add("Disabling because altitude too high.");
				Disable();
				return true;
			}
			if (_currentSpeed > _maxSpeedInCell)
				_maxSpeedInCell = _currentSpeed;
			return false;
		}

		/// <summary>Called rarely when disabled, checks if we should re-enable or if we should check more often if we should re-enable (based on altitude).</summary>
		private void CoarseCheck()
		{
			_activeVessel = FlightGlobals.ActiveVessel;
			if (_activeVessel == null)
				return;
			if (_body == null)
			{
				_body = _activeVessel.mainBody;
				_reEnableSpeed = _initialReEnableSpeed;
				Log.Add("Body is now {0}.", _body.bodyName);
			}
			else if (_body.flightGlobalsIndex != _activeVessel.mainBody.flightGlobalsIndex)
			{
				Log.Add("Changed body from {0} to {1}.", _body.bodyName, _activeVessel.mainBody.bodyName);
				_reEnableSpeed = _initialReEnableSpeed;
				_body = _activeVessel.mainBody;
			}
			SetPosLatLonElevAlt();
			if (!_manualDisable && _prevActiveVessel != null && !_activeVessel.Equals(_prevActiveVessel))
			{
				Log.Add("restarting because changed vessel from {0} to {1}.", _prevActiveVessel.vesselName, _activeVessel.vesselName);
				VesselChanged();
				return;
			}
			_prevActiveVessel = _activeVessel;
			if (_vesselRealAltitude <= _fineCheckAltitude)
			{
				_fineChecks = true;
				_dtAccumulator2 = 0.000f;
#if DEBUG
				Log.Add("(disabled): Terrain altitude is {0}, switching to FineCheck.", _activeVessel.terrainAltitude);
#endif
			}
		}

		void Update()
		{
			if (_activeVessel != null)
			{
				_currentSpeed = _activeVessel.GetSrfVelocity().magnitude;
				if (_enabled)
				{
					if (_currentSpeed > _fastCheckVelocity && _checkFrequencySeconds == _slowCheckFrequencySeconds)
						_checkFrequencySeconds = _fastCheckFrequencySeconds;
					else if (_currentSpeed <= _slowCheckVelocity && _checkFrequencySeconds == _fastCheckFrequencySeconds)
						_checkFrequencySeconds = _slowCheckFrequencySeconds;
				}
			}
			_dt = Time.deltaTime;
			if (_fineChecks)
			{
				_dtAccumulator2 += _dt;
				if (_dtAccumulator2 * 1000f >= _fineCheckFrequencyMilliseconds)
				{
					_dtAccumulator2 = 0.000f;
					if (FineCheck())
						return;
				}
			}
			_dtAccumulator += _dt;
			if (_dtAccumulator >= _checkFrequencySeconds && !_movingRocks && !_starting && !_disabling)
			{
				_dtAccumulator = 0;
				if (!_fineChecks)
					CoarseCheck();
				else if (_enabled)
				{
					_cx = Mathf.FloorToInt((float)_vLon / _cellWidth);
					_cy = Mathf.FloorToInt((float)_vLat / _cellWidth);
					_ccLon = ((float)_cx) * _cellWidth;
					_ccLat = ((float)_cy) * _cellWidth;
					StartCoroutine(Move());
				}
			}
			if (_enabled)
			{
				_dtAccumulator3 += _dt;
				if (_dtAccumulator3 >= _rockMovingCheckFrequencySeconds)
				{
					_dtAccumulator3 = 0;
					DebugCountActive();
					if (!_checkingMoves && _currentSpeed <= _maxRockMoveSpeed)//if you bump into a rock while driving really fast, you probably don't really care about that rock, we're not turning it into a vessel.
						StartCoroutine(CheckIfRocksMoved());
				}
			}
		}

		/// <summary>Moves cells of rocks from behind you to in front of you</summary>
		private IEnumerator Move()
		{
			_movingRocks = true;
			if (_cx != _pcx || _cy != _pcy) //player changed cells
			{
				_debugMovedCellCount = 0;
				_xd = _cx - _pcx;
				_yd = _cy - _pcy;
				_pcx = _cx;
				_pcy = _cy;
				if (Math.Abs(_xd) > 1 || Math.Abs(_yd) > 1)
				{
#if DEBUG
					Log.Add("Disabling because vessel is moving too fast.");
#endif
					_disableSpeed = _maxSpeedInCell;
					if (_disableSpeed < 10f)
						_disableSpeed = 10f;
					_reEnableSpeed = _disableSpeed * _reEnableAtDisableSpeedPercent;
					Disable();
					yield break;
				}
				_maxSpeedInCell = 0.0f;
				if (_xd != 0)
				{
					yield return StartCoroutine(MoveRocksToCell(_xd, 0));
					if (_yd == 0)
					{
						yield return StartCoroutine(MoveRocksToCell(_xd, -1));
						yield return StartCoroutine(MoveRocksToCell(_xd, 1));
					}
					else
					{
						yield return StartCoroutine(MoveRocksToCell(_xd, _yd));
						yield return StartCoroutine(MoveRocksToCell(_xd, -_yd));
					}
				}
				if (_yd != 0)
				{
					yield return StartCoroutine(MoveRocksToCell(0, _yd));
					if (_xd == 0)
					{
						yield return StartCoroutine(MoveRocksToCell(-1, _yd));
						yield return StartCoroutine(MoveRocksToCell(1, _yd));
					}
					else if (_xd > 0)
						yield return StartCoroutine(MoveRocksToCell(-1, _yd));
					else
						yield return StartCoroutine(MoveRocksToCell(1, _yd));
				}
				for (int i = 0; i < _rockInfos.Count; i++)
				{
					_ri = _rockInfos[i];
					if (_ri.Moved)
						_ri.Moved = false;
					else
					{
						_ri.Xd -= _xd;
						_ri.Yd -= _yd;
						if (Math.Abs(_ri.Xd) > 1 || Math.Abs(_ri.Yd) > 1)
							Log.Add("ERROR! Step is {0}:{1}, cell {2}:{3} now has coordinates {4}:{5}.", _xd, _yd, _ri.Xd + _xd, _ri.Yd + _yd, _ri.Xd, _ri.Yd);
					}
				}
				if (!(_debugMovedCellCount == 3 || _debugMovedCellCount == 5))
					Log.Add("ERROR: moved {0} cells in one step! (instead of 3 or 5)", _debugMovedCellCount);
			}
			_movingRocks = false;
		}

		/// <summary>False if GetWorldPos3D failed</summary>
		private bool SetPosLatLonElevAlt()
		{
			try
			{
				_ppos = _activeVessel.GetWorldPos3D();
			}
			catch (NullReferenceException)
			{
				return false;
			}
			_vLat = _body.GetLatitude(_ppos);
			_vLon = _body.GetLongitudeNormalized(_ppos);
			_vesselElev = KspTools.GetElevation(_body, _vLat, _vLon);
			if (_vesselElev < 0)
				_vesselElev = 0;
			_vesselRealAltitude = _activeVessel.altitude - _vesselElev;
			return true;
		}

		/// <summary>
		/// Sets the values of the _ri RockInfo object, based on the randomized values in the corresponding RockRandomization object in the _rockRandoms array (pointed out by the given index).
		/// </summary>
		/// <param name="xd">The rock's cell's x-coordinate cell difference from active vessel</param>
		/// <param name="yd">The rock's cell's y-coordinate cell difference from active vessel<</param>
		/// <param name="index">The index of the rock in the cell</param>
		private void SetRiValues(int xd, int yd, int index)
		{
			_rockRand = _rockRandoms[index];
			_ri.IndexInCell = index;
			_ri.Lat = _ccLat + (yd * _cellWidth) + (_rockRand.LatitudeSelector * _cellWidth);
			_ri.Lon = _ccLon + (xd * _cellWidth) + (_rockRand.LongitudeSelector * _cellWidth);
			_ri.RotationDegrees = _rockRand.RotationDegrees;
			if (_rockRand.MovabilitySelector < _unmovableRockProbability)
			{
				_ri.IsMovable = false;
				_ri.Scale = (float)(_unmovableRockScaleMin + ((_unmovableRockScaleMax - _unmovableRockScaleMin) * _rockRand.ScaleModifier));
			}
			else
			{
				_ri.IsMovable = true;
				_ri.Scale = (float)(_movableRockScaleMin + ((_movableRockScaleMax - _movableRockScaleMin) * _rockRand.ScaleModifier));
			}
			_ri.TextureIndexSelector = _rockRand.TextureIndexSelector;
			_ri.Data.OriginBody = _body;
			_biome = _body.GetBiome(_ri.Lat, _ri.Lon);
			_ri.Data.BiomeIndex = _body.GetBiomeIndex(_biome);
			_ri.Data.BiomeName = _biome.name;
			_ri.Data.Type = RockType.Choose(_rockRand.RockTypeSelector, _rockRand.ProminentRockChoice, _biome, _body.flightGlobalsIndex, _prominentInCell);
		}

		/// <summary>All randomizations for a cell need to be in here.
		/// Helps keep track of when randoms are generated, they need to always be in the same order and with the same parameters, for determinism.</summary>
		private void SetCellRandoms()
		{
			_biome = _body.GetBiome((_cellY * _cellWidth) + _cellWidthHalf, (_cellX * _cellWidth) + _cellWidthHalf);
			_r = new System.Random(_baseSeed + KspTools.GetSeedForCell(_body, _cellX, _cellY));
			_cellRockDensity = _r.NextDouble();
			_activeRocksInCell = _rocksPerCell - _r.Next(_maxDeactivatedRocksPerCell);
			if (_cellRockDensity > _lowerRockDensityChance)
				_activeRocksInCell = _rocksPerCell;
			int maxDensity = (int)(((float)RockType.GetRockDensity(_body.flightGlobalsIndex, _biome) / 100f) * (float)_rocksPerCell);
			if (_activeRocksInCell > maxDensity)
				_activeRocksInCell = maxDensity;
			_cellHasProminentRock = _r.NextDouble() < _cellHasProminentRockChance;
			_typeSelector = _r.Next(100);
			_classSelector = _r.Next(3);
			_prominentInCell = RockType.GetProminentTypeForArea(_typeSelector, _body.flightGlobalsIndex, _biome, _vLat, ref _cellHasProminentRock);
			for (int i = 0; i < _rocksPerCell; i++)
				_rockRandoms[i].Set(_r, _rockPrefabs.Count);
		}

		/// <summary>
		/// Moves a _rocksPerCell number of rocks from one of the cells that were "left behind" by the active vessel, to their new positions in the specified cell.
		/// </summary>
		/// <param name="xd">x coordinate of the cell to move to</param>
		/// <param name="yd">y coordinate of the cell to move to</param>
		/// <param name="duringReEnable">If true, the method won't search for "left behind" cells, it will move the first rocks it finds. Useful when you need to move all the rocks anyway (at re-enable).</param>
		private IEnumerator MoveRocksToCell(int xd, int yd, bool duringReEnable = false)
		{
			_cellX = _cx + xd;
			_cellY = _cy + yd;
			_cellHasMovedRock = null;
			_indexInCell = 0;
			//move all rocks from that cell into pseudorandom position in new cell (xd,yd)
			SetCellRandoms();
			_counter = 0;
			for (int i = 0; i < _fakeRocks.Count; i++)
			{
				_fr = _fakeRocks[i];
				_ri = _rockInfos[i];
				if (!_ri.Moved && (duringReEnable || (Math.Abs(_ri.Xd - _xd) > 1) || (Math.Abs(_ri.Yd - _yd) > 1))
					&& _ri.PrefabIndex == _counter)
				{
					_ri.Xd = xd;
					_ri.Yd = yd;
					SetRiValues(xd, yd, _counter);
					_isMovedRock = false;
					_ri.IndexInCell = _indexInCell;
					if (Config.IsInForbiddenArea(_ri, _body) || _counter >= _activeRocksInCell)
						_fr.SetActiveReally(false);
					else
					{
						if (!_cellHasMovedRock.HasValue || _cellHasMovedRock.Value)
						{
							for (int j = 0; j < _realRockInfos.Count; j++)
							{
								_rri = _realRockInfos[j];
								if (_rri == null)
									continue;
								if (_rri.BodyIndex == _body.flightGlobalsIndex && _rri.Cx == _cellX && _rri.Cy == _cellY)
								{
									_cellHasMovedRock = true;
									if (_rri.IndexInCell == _indexInCell)
									{
										_isMovedRock = true;
										break;
									}
								}
							}
							if (!_cellHasMovedRock.HasValue)
								_cellHasMovedRock = false;
						}
						if (_isMovedRock || _body.IsInWater(_ri.Lat, _ri.Lon))
							_fr.SetActiveReally(false);
						else
						{
							_fr.SetActiveReally(true);
							SetSpawnTransform(_ri);
							_fr.rigidbody.mass = _ri.CalculateMass();
							_fr.renderer.material.mainTexture = _ri.GetTexture();
							_fr.rigidbody.isKinematic = !_ri.IsMovable;
							_fr.transform.localScale = new Vector3(_ri.Scale, _ri.Scale, _ri.Scale);
							_fr.transform.position = _spawnTransform.position;
							_fr.transform.rotation = _spawnTransform.rotation;
						}
					}
					_ri.Moved = true;
					_counter++;
					_indexInCell++;
					yield return null;
				}
				if (duringReEnable && _counter == _rocksPerCell)
					break;
			}
			if (_counter != _rocksPerCell)
				Log.Add("ERROR: couldn't find enough rocks for cell {0},{1}.", _cellX, _cellY);
			_debugMovedCellCount++;
		}

		/// <summary>_ccLon and _ccLat need to be up to date when calling this!</summary>
		private IEnumerator SpawnRocksInCell(int xd, int yd)
		{
			_cellHasMovedRock = null;
			_cellX = _cx + xd;
			_cellY = _cy + yd;
			SetCellRandoms();
			for (int k = 0; k < _rocksPerCell; k++)
			{
				_ri = new RockInfo();
				SetRiValues(xd, yd, k);
				_rockRand = _rockRandoms[k];
				SetSpawnTransform(_ri);
				_fr = (GameObject)Instantiate(_rockPrefabs[k], _spawnTransform.position, _spawnTransform.rotation);
				if (_fr == null)
				{
					Log.Add("ERROR: failed to instantiate rock object!");
					yield break;
				}
				if (_fr.collider == null)
					Log.Add("ERROR: rock object doesn't have a collider!");
				if (!_body.IsInWater(_ri.Lat, _ri.Lon) && /*Vector3.Distance(_spawnTransform.position, _activeVessel.transform.position) > 3 &&*/ !_activeVessel.ContainsCollider(_fr.collider))
					_fr.SetActiveReally(true);
				else
					_fr.SetActiveReally(false);
				_fr.rigidbody.mass = _ri.CalculateMass();
				_fr.renderer.material.mainTexture = _ri.GetTexture();
				_ri.Moved = false;
				_ri.Xd = xd;
				_ri.Yd = yd;
				_ri.PrefabIndex = k;
				_ri.IndexInCell = k;
				_fr.rigidbody.isKinematic = !_ri.IsMovable;
				_fr.transform.localScale = new Vector3(_ri.Scale, _ri.Scale, _ri.Scale);
				_fakeRocks.Add(_fr);
				_rockInfos.Add(_ri);
				if (Config.IsInForbiddenArea(_ri, _body) || k >= _activeRocksInCell)
					_fr.SetActiveReally(false);
				else if (!_cellHasMovedRock.HasValue || _cellHasMovedRock.Value)
				{
					for (int j = 0; j < _realRockInfos.Count; j++)
					{
						_rri = _realRockInfos[j];
						if (_rri == null)
							continue;
						if (_rri.BodyIndex == _body.flightGlobalsIndex && _rri.Cx == _cellX && _rri.Cy == _cellY)
						{
							_cellHasMovedRock = true;
							if (_rri.IndexInCell == k)
							{
								_fr.SetActiveReally(false);
								break;
							}
						}
					}
					if (!_cellHasMovedRock.HasValue)
						_cellHasMovedRock = false;
				}
				yield return null;
			}
		}

		private void SetSpawnTransform(RockInfo ri)
		{
			_elevation = KspTools.GetElevation(_body, ri.Lat, ri.Lon);
			if (_elevation < 0)
				_elevation = 0;
			if (ri.IsMovable && KspTools.IsTooSloped(_body, ri.Lat, ri.Lon, _elevation, _cellWidth))
				ri.IsMovable = false;
			_extraSpawnHeight = ri.IsMovable ? _extraSpawnHeightMovable : _extraSpawnHeightImmovable;
			_spawnSurfacePos = _body.GetWorldSurfacePosition(ri.Lat, ri.Lon, _elevation + _extraSpawnHeight);
			if (!ri.IsMovable)
			{
				if (Physics.Raycast(_spawnSurfacePos, _body.GetSurfaceNVector(ri.Lat, ri.Lon) * -1, out _hit)) //fixes rocks floating due to inaccurate elevation data
					_spawnSurfacePos = _hit.point;
			}
			ri.Lat = _body.GetLatitude(_spawnSurfacePos);
			ri.Lon = _body.GetLongitudeNormalized(_spawnSurfacePos);
			_emptyGameObject.transform.position = new Vector3((float)_spawnSurfacePos.x, (float)_spawnSurfacePos.y, (float)_spawnSurfacePos.z);
			_emptyGameObject.transform.rotation = Quaternion.AngleAxis(ri.RotationDegrees, Vector3.up);
			_spawnTransform = _emptyGameObject.transform;
		}

		private RockPart SpawnRockVessel(RockInfo ri, GameObject fr)
		{
			fr.SetActiveReally(false);
			_rockPart = (Part)UnityEngine.Object.Instantiate(_rockAvailableParts[ri.PrefabIndex].partPrefab);
			if (_rockPart == null)
			{
				Log.Add("ERROR: SpawnRockVessel: failed to instantiate rock part!");
				return null;
			}
			_rockPart.gameObject.SetActive(true);
			_rockPart.gameObject.name = _ap.name;
			_rockPart.partInfo = _ap;
			_rockPart.isPersistent = true;
			_rp = _rockPart.GetComponent<RockPart>();
			RealRockInfo rri = new RealRockInfo()
			{
				Cx = Mathf.FloorToInt((float)ri.Lon / _cellWidth),
				Cy = Mathf.FloorToInt((float)ri.Lat / _cellWidth),
				BodyIndex = _body.flightGlobalsIndex,
				IndexInCell = ri.IndexInCell,
				Data = ri.Data,
				Scale = ri.Scale
			};
			_rockPart.mass = ri.CalculateMass();
			_rockPart.transform.GetChild(0).localScale = new Vector3(ri.Scale, ri.Scale, ri.Scale);
			AddRri(rri);
			_debugRriCount++;
			_rp.Rri = rri;
			_rp.Serialize();
			if (_rockPart.rigidbody != null)
			{
				_rockPart.rigidbody.velocity = fr.rigidbody.velocity;
				_rockPart.rigidbody.angularVelocity = fr.rigidbody.angularVelocity;
			}
			_rockSc = new ShipConstruct { _rockPart };
			_rockSc.shipName = rri.Data.Type.VesselName;
			_rockSc.SaveShip();
			ShipConstruction.PutShipToGround(_rockSc, fr.transform);
			_rockVessel = _rockSc.parts[0].localRoot.gameObject.AddComponent<Vessel>();
			_rockVessel.vesselName = _rockSc.shipName;
			_rockVessel.Initialize();
			_rockVessel.Landed = true;
			_rockVessel.vesselType = VesselType.Unknown;
			_rockVessel.isPersistent = true;
			_rockVessel.DiscoveryInfo.SetLevel(DiscoveryLevels.Unowned);
			for (var i = 0; i < _rockPart.Modules.Count; i++)
			{
				var node = new ConfigNode();
				node.AddValue("name", _rockPart.Modules[i].moduleName);
				var j = i;
				_rockPart.LoadModule(node, ref j);
			}
			_rp.Rri.TextureIndex = ri.GetTextureIndex();
			_texture = ri.GetTexture();
			_renderers = _rockPart.FindModelComponents<Renderer>();
			foreach (Renderer renderer in _renderers)
				renderer.material.mainTexture = _texture;
#if DEBUG
			Log.Add("created rock: scale: {0}, mass {1}", _ri.Scale, _rockPart.mass);
#endif
			//try to make sure it doesn't fall through the ground
			_rockVessel.SetPosition(fr.transform.position, true);
			return _rp;
		}

		private IEnumerator ClearAll()
		{
			for (int i = 0; i < _fakeRocks.Count; i++)
			{
				Destroy(_fakeRocks[i]);
				yield return null;
			}
			_fakeRocks.Clear();
			_rockInfos.Clear();
			_realRockInfos.Clear();
			_debugRriCount = 0;
		}

		private IEnumerator DeactivateAll()
		{
			for (int i = 0; i < _fakeRocks.Count; i++)
			{
				_fakeRocks[i].SetActiveReally(false);
				yield return null;
			}
		}

		private void InitializeForEnabling()
		{
			_cellWidth = KspTools.GetCellWidthForBody(_body);
#if DEBUG
			Log.Add("Cell width for {0} is {1} (radius: {2}, Kerbin radius: {3}).", _body.bodyName, _cellWidth, _body.Radius, FlightGlobals.Bodies[1].Radius);
#endif
			_cellWidthHalf = _cellWidth / 2f;
			_checkFrequencySeconds = _fastCheckFrequencySeconds;
			_fineChecks = true;
			_dtAccumulator2 = 0.000f;
			_movingRocks = false;
			_dtAccumulator = 0;
			_maxSpeedInCell = 0.0f;
			_enabled = true;
			_reEnableSpeed = _disableSpeed * _reEnableAtDisableSpeedPercent;
			_ppos = _activeVessel.GetWorldPos3D();
			_cx = _pcx = Mathf.FloorToInt((float)_body.GetLongitudeNormalized(_ppos) / _cellWidth);
			_cy = _pcy = Mathf.FloorToInt((float)_body.GetLatitude(_ppos) / _cellWidth);
			_ccLon = ((float)_cx) * _cellWidth;
			_ccLat = ((float)_cy) * _cellWidth;
			_activeRocksInCell = 0;
			_checkingMoves = false;
			_showConfirmWindow = _showExamineFailureWindow = false;
			SelectedRockData = null;

			//debug:
			_debugText = string.Empty;
			_debugActiveCount = _debugMovedCellCount = 0;
		}

		private bool DoEnableChecks()
		{
			_activeVessel = FlightGlobals.ActiveVessel;
			if (_activeVessel == null)
				return false;
			_prevActiveVessel = _activeVessel;
			_body = _activeVessel.mainBody;
			if (_body == null || Config.ForbiddenBodyIndices.Contains(_body.flightGlobalsIndex))
				return false;
			return SetPosLatLonElevAlt() && _vesselRealAltitude <= _disableAltitude && _activeVessel.GetSrfVelocity().magnitude <= _disableSpeed;
		}

		/// <summary>Checks for the necessary conditions, then spawns all the rocks around the active vessel.</summary>
		private IEnumerator TryEnable()
		{
			_starting = true;
			_reEnableSpeed = _initialReEnableSpeed;
			if (!DoEnableChecks())
			{
				_starting = false;
#if DEBUG
				Log.Add("TryEnable: NOT enabling on body {0} for vessel {1}.", _body.bodyName, _activeVessel.vesselName);
#endif
				yield break;
			}
			InitializeForEnabling();
#if DEBUG
			Log.Add("TryEnable: Enabling on body {0} for vessel {1}.", _body.bodyName, _activeVessel.vesselName);
#endif
			yield return StartCoroutine(ClearAll());
			Load();
			for (int i = -1; i <= 1; i++)
			{
				for (int j = -1; j <= 1; j++)
					yield return StartCoroutine(SpawnRocksInCell(i, j));
			}
			_starting = false;
		}

		/// <summary>Same as TryEnable, but when we already have all the rocks, we just need to move them to new locations.</summary>
		private IEnumerator ReEnable()
		{
			_starting = true;
			if (_fakeRocks.Count == 0)
			{
				yield return StartCoroutine(TryEnable());
				yield break;
			}
			if (!DoEnableChecks())
			{
				_starting = false;
				yield break;
			}
			InitializeForEnabling();
#if DEBUG
			Log.Add("ReEnable: ReEnabling on body {0} for vessel {1}.", _body.bodyName, _activeVessel.vesselName);
#endif
			Load();
			for (int i = 0; i < _rockInfos.Count; i++)
				_rockInfos[i].Moved = false;
			for (int i = -1; i <= 1; i++)
			{
				for (int j = -1; j <= 1; j++)
					yield return StartCoroutine(MoveRocksToCell(i, j, duringReEnable: true));
			}
			for (int i = 0; i < _rockInfos.Count; i++)
				_rockInfos[i].Moved = false;
			_starting = false;
		}

		private void ManualDisable()
		{
			_manualDisable = true;
			Log.Add("Disabling because user clicked disable button.");
			Disable();
		}

		private void Disable()
		{
			Log.Add("Disabling...");
			_disabling = true;
			_enabled = false;
			_movingRocks = false;
			_checkFrequencySeconds = _slowCheckFrequencySeconds;
			StartCoroutine(DeactivateAll());
			Log.Add("Disabled.");
			_disabling = false;
		}

		/// <summary>Checks all fake rocks, if one of them moved, it'll be a moved rock, with a saved location, and the random seed-based moving/spawning won't put a rock in its original location.</summary>
		private IEnumerator CheckIfRocksMoved()
		{
			if (_fakeRocks == null || _rockInfos == null || _body == null)
				yield break;
			_checkingMoves = true;
			for (int i = 0; i < _fakeRocks.Count; i++)
			{
				_ri = _rockInfos[i];
				if (!_ri.IsMovable)
					continue;
				_fr = _fakeRocks[i];
				if (_fr == null || !_fr.activeInHierarchy)
					continue;
				_rlat = _body.GetLatitude(_fr.transform.position);
				_rlon = _body.GetLongitudeNormalized(_fr.transform.position);
				_rlon2 = _body.GetLongitude(_fr.transform.position);
				if (FlightGlobals.getAltitudeAtPos(_fr.transform.position) - KspTools.GetElevation(_body, _rlat, _rlon2) < _undergroundRockThreshold)
				{
#if DEBUG
					Log.Add("CheckIfRocksMoved: Deactivated fake rock {0} because it was underground by {1}m. prefab {2}, scale: {3}", i,
						FlightGlobals.getAltitudeAtPos(_fr.transform.position) - KspTools.GetElevation(_body, _rlat, _rlon2),
						_ri.PrefabIndex, _ri.Scale
						);
#endif
					_fr.SetActiveReally(false);
					continue;
				}
				if (Math.Abs(_rlat - _ri.Lat) > _rockMoveThreshold || Math.Abs(_rlon - _ri.Lon) > _rockMoveThreshold) //moved at least a little
				{
					if ((Math.Abs(_rlat - _vLat) < _rockMoveMaxPlayerDistance && Math.Abs(_rlon - _vLon) < _rockMoveMaxPlayerDistance)) //not too far from player (in that case, it was just rolling off a hill)
					{
						SpawnRockVessel(_ri, _fr);
						_debugRealRockSpawnedCount++;
						yield return null;
					}
					else //moved, but too far from player: let's say it didn't move.
					{
						_ri.Lat = _rlat;
						_ri.Lon = _rlon;
					}
				}
			}
			_checkingMoves = false;
		}

		private void VesselChanged()
		{
			bool needsReEnable = Vector3.Distance(_prevActiveVessel.GetWorldPos3D(), _activeVessel.GetWorldPos3D()) > _vesselChangeRestartDistance;
			_prevActiveVessel = _activeVessel;
			if (needsReEnable)
			{
				Log.Add("Vessel changed, reenabling...");
				StartCoroutine(ReEnable());
			}
			else
				Log.Add("Vessel changed, but not reenabling because closer than {0}m", _vesselChangeRestartDistance);
		}

		private void OnDestroy()
		{
			if (_guiButton != null)
			{
				ApplicationLauncher.Instance.RemoveModApplication(_guiButton);
				_guiButton = null;
			}
		}

		private void Load()
		{
			RockPart rp;
#if KIS
			RealRockInfo rri = null;
			List<ModuleKISInventory> invs = new List<ModuleKISInventory>();
			ModuleKISInventory[] arr;
			string versionString = RealRockInfo.CurrentVersion + '|';
#endif
			List<Vessel> toKill = null;//DEBUG: probably not needed
			int count = 0;
			foreach (Vessel v in FlightGlobals.Vessels)
			{
				rp = v.GetComponent<RockPart>();
				if (rp != null)
				{
					rp.Deserialize();
					if (rp.Rri == null)
					{
						if (toKill == null)
							toKill = new List<Vessel>();
						toKill.Add(v);
					}
					AddRri(rp.Rri);
					count++;
				}
#if KIS
				else
				{
					invs.Clear();
					foreach (Part p in v.Parts)
					{
						arr = p.GetComponents<ModuleKISInventory>();
						if (arr == null || arr.Length == 0)
							arr = p.GetComponentsInChildren<ModuleKISInventory>();
						if (arr != null && arr.Length > 0)
							invs.AddRange(arr);
					}
					foreach (ModuleKISInventory inv in invs)
					{
						foreach (var item in inv.items.Values)
						{
							foreach (ConfigNode node in item.availablePart.partConfig.nodes)
							{
								if (node.values.Count > 0 && node.values[0].value.Equals("RockPart")) //we could determine this by: item.availablePart.partUrl.StartsWith("SurfaceRocks/Parts/rock")
								{																	  //but this way is a bit more robust maybe? or nicer? it's ugly either way.
									string ser = null;
									foreach (ConfigNode n in item.partNode.nodes)
									{
										foreach (ConfigNode.Value val in n.values)
										{
											if (val.name.Equals("RockInfoString"))
											{
												ser = val.value;
												break;
											}
										}
										if (ser != null)
											break;
									}
									if (!string.IsNullOrEmpty(ser) && ser.StartsWith(versionString))
									{
										rri = RockPart.Deserialize(ser);
										if (rri != null)
										{
											AddRri(rri);
											count++;
										}
									}
									break;
								}
							}
						}
					}
					continue;
				}
#endif
			}
			if (toKill != null) //DEBUG: probably not needed
			{
				Log.Add("Load: destroying {0} rock vessels that had no rock info", toKill.Count);
				foreach (Vessel v in toKill)
					v.Die();
			}
#if DEBUG
			Log.Add("Load found {0} real rocks. RealRockInfos.Count: {1}", count, _realRockInfos.Count);
#endif
		}

		public static void AddRri(RealRockInfo rri)
		{
			if (!_realRockInfos.Contains(rri))
			{
				_realRockInfos.Add(rri);
				_debugRriCount++;
				RockInfo ri;
				GameObject fr;
				for (int i = 0; i < _rockInfos.Count; i++)
				{
					fr = _fakeRocks[i];
					ri = _rockInfos[i];
					if (fr.activeInHierarchy &&
						_cx + ri.Xd == rri.Cx &&
						_cy + ri.Yd == rri.Cy &&
						ri.IndexInCell == rri.IndexInCell)
					{
						fr.SetActiveReally(false);
						break;
					}
				}
			}
		}

		private void ExamineNearestRock()
		{
			if (_activeVessel.GetSrfVelocity().magnitude > _maxExamineVelocity)
			{
				FailExamination("Moving too fast", "You need to stay still while examining a rock!");
				return;
			}
			bool fake = false;
			GameObject fr;
			RockData data = null;
			_ppos = _activeVessel.GetWorldPos3D();
			float dist, mindistFake = float.MaxValue, mindistReal = float.MaxValue;
			int closestIndex = 0;
			//find closest fake rock
			for (int i = 0; i < _rockInfos.Count; i++)
			{
				fr = _fakeRocks[i];
				if (!fr.activeInHierarchy)
					continue;
				dist = Vector3.Distance(fr.transform.position, _ppos);
				if (dist < mindistFake)
				{
					mindistFake = dist;
					closestIndex = i;
				}
			}
			//find closest real rock
			RockPart rp;
			RockPart closestReal = null;
			foreach (Vessel v in FlightGlobals.Vessels)
			{
				rp = v.GetComponent<RockPart>();
				if (rp != null)
				{
					dist = Vector3.Distance(v.GetWorldPos3D(), _ppos);
					if (dist < mindistFake)
					{
						mindistReal = dist;
						closestReal = rp;
					}
				}
			}
			//see which search yielded results; choose the closest of the two
			if (mindistFake < mindistReal || closestReal == null)
			{
				if (mindistFake <= _maxAnalyzeDistance)
				{
					data = _rockInfos[closestIndex].Data;
					fake = true;
				}
			}
			else if (closestReal != null && mindistReal <= _maxAnalyzeDistance)
				data = closestReal.Rri.Data;
			if (data == null) //no result, show failure window
				FailExamination("Too far away", "You need to get closer to a rock if you want to examine it!");
			else //found closest rock, show examination window & make real if movable
			{
				SelectedRockData = data;
				ExaminedByScientist = _activeVessel.GetVesselCrew().First().experienceTrait.Config.Name.ToLower().Equals(_strScientist);
				_showExamineFailureWindow = false;
				if (fake && _rockInfos[closestIndex].IsMovable)
					SpawnRockVessel(_rockInfos[closestIndex], _fakeRocks[closestIndex]);
			}
		}

		private void FailExamination(string title, string reason)
		{
			_examineFailureTitle = title;
			_examineFailureReason = reason;
			SelectedRockData = null;
			_showExamineFailureWindow = true;
			_showConfirmWindow = false;
		}
	}
}
